<?php

// Copyright 2019-2022 Hackware SpA <human@hackware.cl>
// This file is part of "Hackware Web Services Payment" and licensed under
// the terms of the GNU Affero General Public License version 3, or (at your
// option) a later version. You should have received a copy of this license
// along with the software. If not, see <https://www.gnu.org/licenses/>.

namespace Hawese\Payment;

use Hawese\Core\TableModel;
use Hawese\Core\User;
use Hawese\Payment\Support\Http as HttpSupport;
use Hawese\Payment\Exceptions\UnexpectedResponseException;
use Buzz\Client\FileGetContents as HttpClient;
use Brick\Money\Money;
use Nyholm\Psr7\Factory\Psr17Factory; // Request, Response & Stream factory
use Psr\Http\Message\{ResponseInterface, StreamInterface};
use Illuminate\Database\Query\Builder;
use UnexpectedValueException;

class Payment extends TableModel
{
    public const STATUS_PENDING = 'pending';
    public const STATUS_COMPLETED = 'completed';
    public const STATUS_ABORTED = 'aborted';

    public static $table = 'payments';
    public static $attributes = [
        'uuid' => ['required', 'uuid'],
        'origin' => ['required', 'string', 'max:30'],
        'user_uid' => ['required', 'string', 'min:3', 'max:100'],
        'gateway' => ['required', 'string', 'min:3', 'max:100'],
        'currency' => ['required', 'string', 'size:3'],
        'amount' => ['required', 'integer'],
        'description' => ['nullable', 'string', 'max:255'],
        'detail' => ['nullable', 'max:65535'], // JSON
        'status' => ['required', 'in:pending,completed,aborted'],
        'created_at' => ['nullable', 'date'],
        'updated_at' => ['nullable', 'date'],
    ];

    public static $primary_key = 'uuid';
    protected static $incrementing = false;

    public function getDetail()
    {
        return json_decode($this->data['detail']);
    }

    public function setDetail($value)
    {
        $this->data['detail'] = json_encode($value);
    }

    public function setAmount($value)
    {
        if ($value instanceof Money) {
            $this->data['amount'] = $value->getMinorAmount()->toInt();
            $this->data['currency'] = $value->getCurrency()->getCurrencyCode();
        } else {
            $this->data['amount'] = Money::of($value, $this->currency)
                ->getMinorAmount()->toInt();
        }
    }

    public function getAmountAsMoney()
    {
        return Money::ofMinor($this->data['amount'], $this->data['currency']);
    }

    public function getAmount()
    {
        return $this->amountAsMoney->getAmount();
    }

    /**
     * Switches payment status.
     *
     * To pending, completed or aborted depending on $compareWith value.
     *
     * @param bool $validComplete can be used for other validations
     * that confirm complete state is valid, if false will not update
     * to complete, but will update to aborted or pending.
     */
    public function validateAndUpdateStatus(
        $compareWith,
        array $completeWith,
        array $pendingWith = null,
        bool $validComplete = null
    ): void {
        if (in_array($compareWith, $completeWith)) {
            $this->status = Payment::STATUS_COMPLETED;
        } elseif ($pendingWith && in_array($compareWith, $pendingWith)) {
            $this->status = Payment::STATUS_PENDING;
        } else {
            $this->status = Payment::STATUS_ABORTED;
        }

        $notify_status = config("payment.origins.$this->origin.notify.status");
        if (in_array($this->status, $notify_status)) {
            if (
                $this->status === static::STATUS_COMPLETED &&
                $validComplete === false
            ) {
                return;
            }
            $this->update(['status']);
            $this->notify();
        } else {
            $this->update(['status']);
        }
    }

    /**
     * Internal instant payment notification (IPN)
     *
     * Maybe you're wondering why implement a request and response from
     * the ground up, it's because we process the configuration params
     * only, we don't want gateway implementations fiddling with it.
     */
    public function notify(): void
    {
        $config = config("payment.origins.$this->origin.notify");

        $response = self::notifySendRequest(
            $config['method'],
            $config['uri'],
            $this->notifyParams($config['params']),
            $config['headers']
        );

        if ($response->getStatusCode() != 200) {
            throw new UnexpectedResponseException($response);
        }
    }

    public function notifySendRequest(
        string $method,
        string $uri,
        array $params,
        array $headers
    ): ResponseInterface {
        $psr17Factory = new Psr17Factory();

        if ($method == 'GET') {
            $uri .= '?' . HttpSupport::buildUriQuery($params);
            $params = [];
        } elseif ($method == 'POST') {
            if (
                array_key_exists('Content-Type', $headers)
                and $headers['Content-Type'] == 'application/json'
            ) {
                $params = $psr17Factory->createStream(json_encode($params));
            } else {
                $params = $psr17Factory->createStream(
                    HttpSupport::buildFormQuery($params)
                );
            }
        } else {
            throw new UnexpectedValueException(
                'Request method ' . $method . ' is unsupported.',
                2002
            );
        }

        $request = $psr17Factory
            ->createRequest($method, $uri)
            ->withBody($params);

        foreach ($headers as $key => $value) {
            $request = $request->withHeader($key, $value);
        }

        return (new HttpClient($psr17Factory))->sendRequest($request);
    }

    /**
     * Fills payment notification params with proper data
     */
    public function notifyParams(array &$params): array
    {
        foreach ($params as $key => $value) {
            if (is_array($value)) {
                $params[$key] = $this->notifyParams($value);
            } else {
                $params[$key] = preg_replace_callback(
                    '/payment\.(.*)/',
                    function ($matches) {
                        return $this->{$matches[1]};
                    },
                    $value
                );
            }
        }

        return $params;
    }

    public function isOwner(User $user): bool
    {
        return $user->uid === $this->user_uid;
    }
}
